package cn.enited.devtools.apiaggregator.service.impl;

import cn.enited.devtools.apiaggregator.api.analysis.ApiAnalyzer;
import cn.enited.devtools.apiaggregator.api.cache.ApiCacheManager;
import cn.enited.devtools.apiaggregator.api.cache.CacheSyncManager;
import cn.enited.devtools.apiaggregator.api.utils.SrvRequestUtil;
import cn.enited.devtools.apiaggregator.api.vo.ApiAnalysisObject;
import cn.enited.devtools.apiaggregator.consts.ApiConst;
import cn.enited.devtools.apiaggregator.consts.CommonConst;
import cn.enited.devtools.apiaggregator.consts.ErrorCode;
import cn.enited.devtools.apiaggregator.consts.MatchRegexPolicy;
import cn.enited.devtools.apiaggregator.converter.ApiAggObjectConverter;
import cn.enited.devtools.apiaggregator.dto.ApiAggObjectDto;
import cn.enited.devtools.apiaggregator.dto.ProjectDto;
import cn.enited.devtools.apiaggregator.entity.ApiAggObject;
import cn.enited.devtools.apiaggregator.exception.BizException;
import cn.enited.devtools.apiaggregator.repo.ApiConfRepository;
import cn.enited.devtools.apiaggregator.service.ApiConfService;
import cn.enited.devtools.apiaggregator.service.ProjectService;
import cn.enited.devtools.apiaggregator.utils.NumUtil;
import cn.enited.devtools.apiaggregator.utils.RedissonUtil;
import cn.enited.devtools.apiaggregator.vo.ApiAggObjectParamVo;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
public class ApiConfServiceImpl extends AbstractService<ApiConfRepository, ApiAggObject> implements ApiConfService {

    private ApiConfRepository repo;

    private ApiAnalyzer analyzer;

    private ApiCacheManager cacheManager;

    private CacheSyncManager cacheSyncManager;

    private ApiAggObjectConverter converter;

    private ProjectService projectService;

    private RedissonUtil redissonUtil;

    @Autowired
    public void setRepo(ApiConfRepository repo) {
        this.repo = repo;
    }

    @Override
    public ApiConfRepository getRepo() {
        return this.repo;
    }

    @Autowired
    public void setAnalyzer(ApiAnalyzer analyzer) {
        this.analyzer = analyzer;
    }

    @Autowired
    public void setCacheManager(ApiCacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    @Autowired
    public void setCacheSyncManager(CacheSyncManager cacheSyncManager) {
        this.cacheSyncManager = cacheSyncManager;
    }

    @Autowired
    public void setConverter(ApiAggObjectConverter converter) {
        this.converter = converter;
    }

    @Autowired
    public void setProjectService(ProjectService projectService) {
        this.projectService = projectService;
    }

    @Autowired
    public void setRedissonUtil(RedissonUtil redissonUtil) {
        this.redissonUtil = redissonUtil;
    }

    @Override
    public ApiAggObjectDto get(Long bizId) {
        return repo.getByBizId(bizId).map(converter::entity2Dto)
                .orElseThrow(() -> new BizException(ErrorCode.NOT_EXIST.code(), "API配置不存在"));
    }

    @Override
    public ApiAggObject getEntityByName(String projectName, String apiName) {
        Query query = Query.query(Criteria.where("name").is(apiName).and("projectName").is(projectName)
                .and(CommonConst.ENTITY.DEL_FLAG).is(false));
        return repo.findOne(query)
                .orElseThrow(() -> new BizException(ErrorCode.NOT_EXIST.code(), "API配置不存在"));
    }

    @Override
    public ApiAggObjectDto getByName(String projectName, String apiName) {
        return converter.entity2Dto(getEntityByName(projectName, apiName));
    }

    @Override
    public List<ApiAggObjectDto> findAll(ApiAggObjectParamVo param) {
        Query query = buildFindQuery(param);
        List<ApiAggObject> entities = repo.findAll(query);
        return converter.entities2DtoList(entities);
    }

    @Override
    public Page<ApiAggObjectDto> findPage(ApiAggObjectParamVo param) {
        Query query = buildFindQuery(param);
        Page<ApiAggObject> page = repo.findPage(query, param.getOneBasedPageNumber(), param.getPageSize());
        return convertEntityPageToDtoPage(page, () -> converter.entities2DtoList(page.getContent()));
    }

    @Override
    public Set<String> findAllCompletableApiNames() {
        List<String> projectNames = projectService.getAllProjectNames();
        return repo.findAllCompletableApiNames(projectNames);
    }

    private Query buildFindQuery(ApiAggObjectParamVo param) {
        return buildQuery(param, p -> {
            List<Criteria> criteriaList = new ArrayList<>();
            criteriaList.add(Criteria.where(CommonConst.ENTITY.DEL_FLAG).is(false));
            if (NumUtil.isValidId(param.getBizId())) {
                criteriaList.add(Criteria.where(CommonConst.ENTITY.BIZ_ID).is(param.getBizId()));
            } else if (CollectionUtil.isNotEmpty(param.getBizIds())) {
                criteriaList.add(Criteria.where(CommonConst.ENTITY.BIZ_ID).in(param.getBizIds()));
            }
            if (StrUtil.isNotBlank(param.getName())) {
                criteriaList.add(Criteria.where("name").is(param.getName()));
            }
            if (NumUtil.isValidId(param.getProjectId())) {
                criteriaList.add(Criteria.where("projectId").is(param.getProjectId()));
            }
            if (StrUtil.isNotBlank(param.getDescription())) {
                criteriaList.add(Criteria.where("description").regex(buildMatchRegex(param.getDescription(), MatchRegexPolicy.ALL)));
            }
            if (StrUtil.isNotBlank(param.getServiceId())) {
                criteriaList.add(Criteria.where("services.id").is(param.getServiceId()));
            }
            Criteria criteria = new Criteria();
            criteria.andOperator(criteriaList);
            return criteria;
        });
    }

    /**
     * 校验配置项的逻辑是否正确
     * @param conf
     * @throws BizException
     */
    private void validate(ApiAggObjectDto conf) throws BizException {
        // 是否有重名的service
        long unDuplicatedServices = conf.getServices().stream().map(ApiAggObjectDto.Entry::getId).count();
        if (unDuplicatedServices != conf.getServices().size()) {
            throw new BizException(ErrorCode.INVALID_DATA.code(), "service id有重复项");
        }

        conf.getServices().forEach(srv -> {
            // 检查当method=post时dataType是否为空
            if (requestBodyIsPossible(srv.getMethod()) && StrUtil.isBlank(srv.getDataType())) {
                throw new BizException(ErrorCode.INVALID_DATA.code(), StrUtil.format("service[{}]的dataType不能为空", srv.getId()));
            }
            // 检查当method=post并且dataType=form时data是否配置成了嵌套key-value
            if (MapUtil.isNotEmpty(srv.getData()) && requestBodyIsPossible(srv.getMethod())
                    && StrUtil.equalsIgnoreCase(srv.getDataType(), CommonConst.REQUEST_DATA_TYPE.FORM)) {
                srv.getData().values().forEach(v -> {
                    if (!(v instanceof String) && !(v instanceof Number) && !(v instanceof Boolean)) {
                        throw new BizException(ErrorCode.INVALID_DATA.code(),
                                StrUtil.format("service[{}]的dataType为form，因此data不能配置成复杂JSON对象"));
                    }
                });
            }
        });
    }

    private boolean requestBodyIsPossible(String method) {
        return StrUtil.equalsAny(method, CommonConst.HTTP_METHOD.POST,
                CommonConst.HTTP_METHOD.PUT, CommonConst.HTTP_METHOD.DELETE);
    }

    @Override
    public Long add(ApiAggObjectDto conf) {
        validate(conf);
        ApiAggObject entity = converter.dto2Entity(conf);
        try {
            ProjectDto project = projectService.get(entity.getProjectId());
            entity.setProjectName(project.getName());
            ApiAnalysisObject analysisObject = analyzer.analyse(entity);
            repo.save(entity);
            cacheManager.putAnalysisObject(entity.getProjectName(), entity.getName(), analysisObject);
            cacheSyncManager.publishCacheUpdateEvent(ApiConst.SYNC_TYPE.ANALYSIS, entity.getProjectName(), entity.getName());
            return entity.getBizId();
        } catch (Exception e) {
            log.error("add ApiAggObject conf error:", e);
            repo.delete(entity.getBizId());
            cacheManager.removeAnalysisObject(entity.getProjectName(), entity.getName());
            if (e instanceof BizException) {
                throw e;
            } else {
                throw new BizException(ErrorCode.ERROR.code(), "添加配置失败");
            }
        }
    }

    @Override
    public void update(ApiAggObjectDto conf) {
        validate(conf);
        ApiAggObject entity = converter.dto2Entity(conf);
        ApiAggObject old = repo.getByBizId(entity.getBizId()).orElseThrow(() -> new BizException(ErrorCode.NOT_EXIST.code(), "API配置不存在"));
        entity.setProjectName(old.getProjectName());

        RLock locker = redissonUtil.getLock(StrUtil.format("{}{}", CommonConst.REDIS.UPDATE_KEY_PREFIX, entity.getBizId()));
        try {
            locker.lock(500L, TimeUnit.MILLISECONDS);
            try {
                repo.update(entity);
            } catch (Exception e) {
                log.error("修改ApiAggObject数据库数据时发生异常:", e);
                if (e instanceof BizException) {
                    throw e;
                } else {
                    throw new BizException(ErrorCode.ERROR.code(), "修改配置失败");
                }
            }
            ApiAnalysisObject analysisObject = null;
            try {
                analysisObject = analyzer.analyse(entity);
            } catch (Exception e) {
                log.error("分析修改后的配置项发生异常", e);
                // 还原数据库对象
                repo.update(old);
                throw new BizException(ErrorCode.ERROR.code(), "API配置错误，分析失败");
            }
            if (Objects.nonNull(analysisObject)) {
                cacheManager.putAnalysisObject(entity.getProjectName(), entity.getName(), analysisObject);
                cacheSyncManager.publishCacheUpdateEvent(ApiConst.SYNC_TYPE.ANALYSIS, entity.getProjectName(), entity.getName());
            }
        } finally {
            locker.unlock();
        }
    }

    @Override
    public void delete(Long bizId) {
        repo.getByBizId(bizId).ifPresent(entity -> {
            repo.delete(bizId);
            cacheManager.removeAnalysisObject(entity.getProjectName(), entity.getName());
        });
    }
}
